perf(sequence): add From<Sequence<T>> for Vec<T> with Copy bound - #20
Merged
esteve merged 1 commit intoJun 8, 2026
Merged
Conversation
For Copy element types (which covers all ROS 2 primitive types), this
impl collapses to a single memcpy from the C-owned sequence buffer
into a freshly-allocated Vec, via as_slice().to_vec() (stdlib
specializes <[T]>::to_vec on T: Copy).
The existing route via SequenceIterator::next() does a per-element
read + zeroed-write to keep the sequence safe to drop after iteration,
making into_iter().collect() O(n) Rust-level work where a single
memcpy would suffice. The cost is linear in the sequence length and
becomes the dominant cost for large primitive sequences in image and
point-cloud message types.
Microbench: Sequence<u8> 65536 -> Vec<u8>
into_iter().collect() 23.3 us (2.62 GiB/s)
.into() (this impl) 513 ns (118.89 GiB/s)
~45x speedup
This is consumed by a companion change in rosidl_rust that updates
the codegen template to emit msg.field.into() for primitive sequence
fields in generated from_rmw_message impls.
Refs ros2-rust/ros2_rust#628 improvement (1).
esteve
approved these changes
Jun 8, 2026
Merged
romainreignier
added a commit
to romainreignier/ros-env
that referenced
this pull request
Jun 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is one of a set of 2 PRs that address issue number 2 described in ros2-rust/ros2_rust#628
This PR has to be merged before
ros2-rust/rosidl_rust#26.Problem
2. Element-by-element sequence conversion in
from_rmw_messageThe generated
from_rmw_messagecode convertsSequence<T>toVec<T>via.into_iter().collect(). TheSequenceIterator::next()implementation (rosidl_runtime_rs/src/sequence.rs) does this per element:For a 64 KB
Sequence<u8>, this is 65,536 individual read + zero-write + insert cycles instead of a singlememcpy.Solution
Adds
impl<T: SequenceAlloc + Copy> From<Sequence<T>> for Vec<T>backed byseq.as_slice().to_vec(). ForCopyelement types (which covers all ROS 2 primitive types), this collapses to a singlememcpyfrom the C-owned sequence buffer into a freshly-allocatedVec.The companion PR in
ros2-rust/rosidl_rust#26updates the codegen template to call this impl on the receive side.Microbenchmark
Using Criterion I ran a really quick benchmark between the before and after, just on the conversion from a
Sequence<u8>of size 64K to aVec<u8>. For my machine it gave the following numbers.into_iter().collect()(old)as_slice().to_vec().into()(new publicFromimpl)Speedup of
.into()overinto_iter().collect()at 64 KiB: ~45×.The
.into()form matchesas_slice().to_vec()within ~1%, confirming theFromimpl inlines to the same code path.End-to-end pub/sub throughput
I also ran an end-to-end benchmark where I compared rclrs before and after the change as well as an rclcpp baseline.
The benchmark ran in a single process with one publisher thread sending
std_msgs/UInt8MultiArraymessages and the main thread runs blockingexecutor.spin(). Reliable +keep_last(1000)QoS, defaultrmw_fastrtps_cpp. 5 s runs, median of 3.Note that all the benchmarks below have quite a bit of run-to-run variance. The numbers are to show the trend and not an absolute comparison.
Node subscription (default API)
At 64 KiB this branch gives a 4.4× rclrs throughput improvement and brings rclrs to ~90% of rclcpp single-process parity. At 16 KiB it gives 1.27× and at 0 B - 1 KiB the change is near-noise because the conversion path is short, and small-payload Node throughput is dominated by per-message executor dispatch overhead, see Worker numbers below.
Worker subscription
The same bench with
node.create_worker(()).create_subscription(...), which dispatches sync callbacks directly on the wait-set thread (noBoxFutureallocation, no mpsc hop):With Worker subscriptions, rclrs on this branch exceeds rclcpp single-process Node at 64 KiB (224 k vs 151 k msg/s, ~1.48×). The two fixes compose: the conversion fix removes the per-publish memcpy ceiling, and the Worker path removes the per-receive dispatch ceiling.
Again note that there is quite a bit of run-to-run variance.
While performing those benchmarks I also ran into a fun issue with colcon-ros-cargo: colcon/colcon-ros-cargo#41